/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package shaman.storage.impl.file;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import shaman.storage.api.Filter;
import shaman.storage.impl.stream.gather.GatherOutputStream;
import shaman.storage.impl.stream.gather.GatherPartListener;
import shaman.storage.impl.stream.gather.InputStreamSource;
import shaman.storage.impl.stream.gather.PartStreamFactory;
import shaman.storage.impl.stream.gather.ScatterInputStream;
import shaman.storage.impl.stream.gather.ScatterOutputStream;
import shaman.storage.impl.stream.size.SizeLimitInputStream;
import shaman.storage.impl.stream.size.SizeMeasureOutputStream;

/**
 *
 * @author dmitriy
 */
public class BlockAccessor {

    private final PartStreamFactory fact;
    private final ScatterInputStream in;
    private final GatherOutputStream out;
    private final Filter blockFilter;
    private final Filter fileFilter;
    private final FilePropertiesCache fileCache;
    private final long sizeLimit;

    private long curOffset=-1;
    private ScatterInputStream blockIn;
    private GatherOutputStream blockOut;
    private long blockSize;
    private long writeOffset=0;

    private class BlockStreamFactory implements InputStreamSource {

        public BlockStreamFactory() {
        }


        public InputStream getInputStream(long offset) throws IOException {
            return fact.getInputStream(offset);
        }

    }

    private class BlockAddListener implements GatherPartListener {

        public BlockAddListener() {
        }

        public void partAdded(long beginOffset, long endOffset) {
            writeOffset+=(beginOffset-endOffset);
            blockOut=null;
        }

    }

    private class FileStreamFactory implements InputStreamSource {

        private final long offset;
        
        public FileStreamFactory(long offset) {
            this.offset=offset;
        }
    
        public InputStream getInputStream(long offset) throws IOException {
            InputStream fileStream=fetchBlock(this.offset);
            long curOffset=0;
            while (curOffset<offset)
            {
                long skipped=fileStream.skip(offset-curOffset);
                if (skipped<0)
                    throw new IOException("Unable to seek");
                curOffset+=skipped;
            }
            return fileStream;
        }

        public long getOffset() {
            return offset;
        }
        
    }
    
    private class FileAddListener implements GatherPartListener {

        private final StoredFileProperties file;
        private final long blockOffset;
        private DigestOutputStream digest;
        private SizeMeasureOutputStream sizeMeasure;

        public FileAddListener(StoredFileProperties file, long blockOffset) {
            this.file=file;
            this.blockOffset=blockOffset;
        }

        public void partAdded(long beginOffset, long endOffset) {
            long storedLength=endOffset-beginOffset;
            if (digest!=null)
                file.setHash(digest.getMessageDigest().digest());
            if (sizeMeasure!=null)
                file.setLength(sizeMeasure.getOffset());
            file.setBlockOffset(blockOffset);
            file.setFileOffset(beginOffset);
            file.setFileLength(storedLength);
            try {
                fileCache.writeProperties(file);
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            blockSize+=storedLength;
            if (blockSize>sizeLimit)
            {
                try {
                    blockOut.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                blockOut=null;
            }
        }

        public void setDigestStream(DigestOutputStream digest, SizeMeasureOutputStream sizeMeasure) {
            this.digest = digest;
            this.sizeMeasure=sizeMeasure;
        }

    }

    public BlockAccessor(PartStreamFactory fact, Filter blockFilter, Filter fileFilter) {
        this.fact = fact;
        in=new ScatterInputStream(new BlockStreamFactory());
        out=null;
        this.blockFilter=blockFilter;
        this.fileFilter=fileFilter;
        fileCache=null;
        sizeLimit=0;
    }

    public BlockAccessor(PartStreamFactory fact, Filter blockFilter, Filter fileFilter, long sizeLimit, FilePropertiesCache fileCache) {
        this.fact = fact;
        in=new ScatterInputStream(new BlockStreamFactory());
        out=new GatherOutputStream(new ScatterOutputStream(fact));
        this.blockFilter=blockFilter;
        this.fileFilter=fileFilter;
        this.sizeLimit=sizeLimit;
        this.fileCache=fileCache;
    }

    protected InputStream fetchBlock(long offset) throws IOException {
        InputStream baseStream=in.nextPart(offset, -1);
        InputStream filteredStream=blockFilter.getInputStream(baseStream);
        return filteredStream;
    }

    protected OutputStream storeBlock() throws IOException {
        OutputStream baseStream=out.nextPart(new BlockAddListener());
        OutputStream filteredStream=blockFilter.getOutputStream(baseStream);
        return filteredStream;
    }

    public InputStream fetchFile(StoredFileProperties file) throws IOException {
        long blockOffset=file.getBlockOffset();
        long fileOffset=file.getFileOffset();
        long fileLength=file.getFileLength();
        long length=file.getLength();
        if (curOffset!=blockOffset)
        {
            InputStreamSource iss=new FileStreamFactory(blockOffset);
            blockIn=new ScatterInputStream(iss);
        }
        InputStream baseStream=blockIn.nextPart(fileOffset, fileLength);
        InputStream filteredStream=fileFilter.getInputStream(baseStream);
        InputStream limitStream=new SizeLimitInputStream(filteredStream, length);
        return limitStream;
    }

    public OutputStream storeFile(StoredFileProperties file) throws IOException {
        if (blockOut==null)
        {
            blockOut=new GatherOutputStream(storeBlock());
            blockSize=0;
        }
        MessageDigest digest;
        try {
            digest=MessageDigest.getInstance(FileBackup.DIGEST_ALGORITHM);
        } catch (NoSuchAlgorithmException ex) {
            throw new IOException("Digest algorithm "+FileBackup.DIGEST_ALGORITHM+" not supported");
        }
        FileAddListener listener=new FileAddListener(file, writeOffset);
        OutputStream baseStream=blockOut.nextPart(listener);
        OutputStream filteredStream=fileFilter.getOutputStream(baseStream);
        SizeMeasureOutputStream sizeStream=new SizeMeasureOutputStream(filteredStream);
        DigestOutputStream digestStream=new DigestOutputStream(sizeStream, digest);
        listener.setDigestStream(digestStream, sizeStream);
        return digestStream;
    }

    public void setImmutable() throws IOException {
        if (blockOut!=null)
        {
            blockOut.close();
            blockOut=null;
        }
        out.close();
    }
}
